package org.colswe.junitlab.logica.imp;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.colswe.junitlab.logica.IControlFestivo;
import org.colswe.junitlab.modelo.Festivo;
import org.colswe.junitlab.modelo.Sistema;
import org.colswe.junitlab.modelo.TipoDia;

/**
 * Clase que realiza los cálculos concernientes a la cantidad de horas y días trabajados.
 * @author Miguel Ángel Cubides
 *
 */
public class ControlFestivo implements IControlFestivo {
	
	/**
	 * Constantes HORAS_NORMAL, HORAS_FESTIVO, HORAS_SABADO, indican la cantidad de horas
	 * equivalentes a cada tipo de día.
	 * festivoL y festivoNL cantidad de festivos laborables y no laborables.
	 */
	private static final Integer HORAS_NORMAL= 8;
	private static final Integer HORAS_FESTIVO = HORAS_NORMAL*2;
	private static final Integer HORAS_SABADO = 4;
	private int festivoL;
	private int festivoNL;
	//Obtiene la única instancia del sistema
	private Sistema sistema = Sistema.getInstance();
	/**
	 * Método que devuelve la cantidad de festivos laborales
	 * @return número de festivos laborales
	 */
	public int getFestivoL() {
		return festivoL;
	}
	/**
	 * Método que asigna un valor a la cantidad de festivos laborales
	 * @param cantidad de festivos laborales
	 */
	public void setFestivoL(int festivoL) {
		this.festivoL = festivoL;
	}
	/**
	 * Método que devuelve la cantidad de festivos no laborales
	 * @return número de festivos no laborales
	 */
	public int getFestivoNL() {
		return festivoNL;
	}
	/**
	 * Método que asigna un valor a la cantidad de festivos no laborales
	 * @param cantidad de festivos no laborales
	 */
	public void setFestivoNL(int festivoNL) {
		this.festivoNL = festivoNL;
	}
	/**
	 * Método que devuelve la instancia del sistema.
	 * 
	 * @return instancia del sistema
	 */
	public Sistema getSistema() {
		return sistema;
	}

	/**
	 * Método contarHoras, recibe 4 parámetros y calcula la cantidad de horas
	 * trabajadas en dos intervalos de tiempo diferentes.
	 * @param desde1 Día inicial, primer intervalo
	 * @param hasta1 Día final, primer intervalo
	 * @param desde2 Día inicial, segundo intervalo
	 * @param hasta2 Día final, segundo intervalo
	 * @return Cantidad de horas totales laboradas en los dos intervalos de tiempo 
	 */
	public Integer contarHoras(Date desde1, Date hasta1, Date desde2,
			Date hasta2) {
		Map<TipoDia, Integer> dias = obtenerDias(desde1,hasta1);
		Map<TipoDia, Integer> dias1 = obtenerDias(desde2,hasta2);
		/*
		 * Se hace el llamado al método contarHoras(Map<TipoDia,Integer>) para cada uno de los
		 * intervalos y se suman ambas cantidades
		 */
		return contarHoras(dias)+contarHoras(dias1);	
	}
	
	/**
	 * Método contarHoras, recibe 2 parámetros y calcula la cantidad de horas
	 * trabajadas en un intervalo de tiempo.
	 * @param desde Día inicial
	 * @param hasta Día final
	 * @return Cantidad de horas totales laboradas en el intervalo de tiempo
	 */
	public Integer contarHoras(Date desde, Date hasta) {
		Map<TipoDia, Integer> dias = obtenerDias(desde,hasta);
		/*
		 * Se hace el llamado al método contarHoras(Map<TipoDia,Integer>).
		 */
		return contarHoras(dias);
	}	

	/**
	 * Método contarHoras, recibe 1 mapa y calcula la cantidad de horas trabajadas
	 * @param info Mapa con la cantidad de días laborados de cada tipo
	 * @return Cantidad de horas totales laboradas en los días especificados en el mapa. 
	 */
	public Integer contarHoras(Map<TipoDia, Integer> info) {
		Integer horas=0;
		/*
		 * Se obtiene cada uno de los tipos de día del mapa que ingresa como parámetro
		 * al método y se multiplica por la cantidad de horas correspondiente a cada
		 * constante definida al inicio de la clase
		 */
		horas+=(info.get(TipoDia.FESTIVO_LABORA)*HORAS_FESTIVO);
		horas+=(info.get(TipoDia.NORMAL)*HORAS_NORMAL);
		horas+=(info.get(TipoDia.SABADO)*HORAS_SABADO);
		return horas;
	}

	/**
	 * Método fechasValidas, recibe 2 parámetros y verifica si las fechas no son nulas
	 * y si desde es menor a hasta
	 * @param desde Fecha inicial
	 * @param hasta Fecha final
	 * @return Valor booleano. Verdadero si desde es menor a hasta, Falso si no es así.
	 */
	public boolean fechasValidas(Date desde, Date hasta) {
		if (desde==null || hasta==null){
			return false;
		}
		return desde.before(hasta);
	}

	/**
	 * Método obtenerDías calcula la cantidad de días de cada tipo.
	 * @param desde Día inicial inclusive
	 * @param hasta Día final inclusive
	 * @return mapa con la cantidad de días separado por tipo de cada día.
	 */
	public Map<TipoDia, Integer> obtenerDias(Date desde, Date hasta) {
		/*
		 * Se inicializa el mapa que será retornado al final del método con cada uno
		 * de los tipos de día y con cantidad cero (0)
		 */
		HashMap<TipoDia, Integer> ret = new HashMap<TipoDia, Integer>();
		ret.put(TipoDia.FESTIVO_LABORA, 0);
		ret.put(TipoDia.FESTIVO_NO_LABORAL, 0);
		ret.put(TipoDia.NORMAL, 0);
		ret.put(TipoDia.SABADO, 0);
		
		//Validación de las fechas.
		if (!fechasValidas(desde, hasta)){
			return ret;
		}
		Calendar cH = Calendar.getInstance();
		cH.setTime(hasta);
		cH.add(Calendar.DATE, 1);
		Calendar c = Calendar.getInstance();
		c.setTimeInMillis(desde.getTime());
		int sabado=0;
		festivoL=0;
		festivoNL=0;
		int normal=0;

		while (cH.after(c)){
			//Primero se cuentan los festivos laborables y no laborables
			boolean festivo = contarFestivos(c);
			//Se cuentan los sábados y días normales después.
			if (!festivo && c.get(Calendar.DAY_OF_WEEK)==Calendar.SATURDAY){
				sabado++;
			}else if (!festivo && c.get(Calendar.DAY_OF_WEEK)!=Calendar.SUNDAY){
				normal++;
			}
			c.add(Calendar.DATE, 1);
		}
		/*
		 * Se límpia el mapa y finalmente se le asigna el valor a cada tipo de día
		 * después de realizados los cálculos 
		 */
		
		ret.clear();
		ret.put(TipoDia.FESTIVO_LABORA, festivoL);
		ret.put(TipoDia.FESTIVO_NO_LABORAL, festivoNL);
		ret.put(TipoDia.NORMAL, normal);
		ret.put(TipoDia.SABADO, sabado);

		return ret;
	}
	
	/**
	 * Método contarFestivos calcula la cantidad de festivos laborables y no laborables. 
	 * @param c Calendario iniciado con la fecha inicial
	 * @return 
	 */
	public boolean contarFestivos(Calendar c){
		boolean festivo=false;
		/*
		 * Se revisa si los festivos son laborables o no y se va realizando la cuenta de cada
		 * uno. Se tienen en cuenta los días domingos como festivos no laborables, a menos que 
		 * se especifique lo contrario.
		 */
		for (Festivo f : sistema.getEntidades()){
			if (f.getFecha().equals(c.getTime())){
				festivo = true;
				if (f.isLaborable()){
					festivoL++;
				}else{
					Calendar ct = Calendar.getInstance();
					ct.setTime(f.getFecha());
					festivoNL += ct.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY ? 0 : 1;
				}
			}
		}
		return festivo;
	}

	/**
	 * Método obtenerFechaFinal: calcula la fecha final con base en la fecha inicial
	 * y la cantidad de horas laborables.
	 * @param inicial Fecha inicial del intervalo
	 * @param horas Cantidad entera de horas laborales 
	 * @return fecha final del intervalo
	 */
	public Date obtenerFechaFinal(Date inicial, Integer horas) {	
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(inicial);
		Integer dif = horas;
		while (dif >= 0){
			if(isFestivoLaboral(calendar.getTime())){
				dif = dif - 16;
				calendar.add(Calendar.DAY_OF_YEAR, 1);
			}else{
				if(isFestivoNoLaboral(calendar.getTime()) || calendar.getTime().getDay() == Calendar.SUNDAY){
					calendar.add(Calendar.DAY_OF_YEAR, 1);
				}else if(calendar.getTime().getDay() == Calendar.SATURDAY){
					dif = dif - 4;
					calendar.add(Calendar.DAY_OF_YEAR, 1);
				}else{
					dif = dif - 8;
					calendar.add(Calendar.DAY_OF_YEAR, 1);
				}
			}
		}
		return calendar.getTime();
	}
	
	
	/**
	 * Método ifFestivoLaboral: Determina si un día es festivo laboral
	 * @param date Fecha a analizar
	 * @return true si es festivo laboral, false en otro caso
	 */
	public boolean isFestivoLaboral(Date date){
		for (Festivo f:sistema.getEntidades()){
			if(f.getFecha().equals(date) && f.isLaborable())
				return true;
		}
		return false;
	}
	
	
	/**
	 * Método ifFestivoNoLaboral: Determina si un día es festivo no laboral
	 * @param date Fecha a analizar
	 * @return true si es festivo no laboral, false en otro caso
	 */
	public boolean isFestivoNoLaboral(Date date){
		for (Festivo f:sistema.getEntidades()){
			if(f.getFecha().equals(date) && !f.isLaborable())
				return true;
		}
		return false;
	}

}
